/*
** Copyright (c) 2009  Kimmo 'Rainy' Pekkola
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program.  If not, see http://www.gnu.org/licenses.
*/

#include "precompiled.h"
#include "rotator.h"
#include "debugtext.h"

#define DRAG_TIMEOUT_RATE 50
#define NUMBER_COUNT 5
#define TAP_AND_HOLD_TIMEOUT 500

//-----------------------------------------------------------------------------
/**
** Constructor.
**
** \param pParent The parent object.
*/
CRotator::CRotator(QObject* pParent) : QObject(pParent)
{
    m_Value = 0;
    m_MaxValue = 60;
    m_Offset = 0;
    m_bEditMode = false;
    m_DragStartOffset = 0;
    m_Speed = 0;

    bool bOk = false;
    bOk = connect(&m_DragTimer, SIGNAL(timeout()), this, SLOT(onTimeout()));
    Q_ASSERT(bOk);
    bOk = connect(&m_HoldTimer, SIGNAL(timeout()), this, SLOT(onHoldTimeout()));
    Q_ASSERT(bOk);

}

//-----------------------------------------------------------------------------
/**
** Destructor.
*/
CRotator::~CRotator()
{
    for (int i = 0; i < m_Numbers.size(); i++)
    {
        delete m_Numbers[i];
    }
}

//-----------------------------------------------------------------------------
/**
** Creates the number items for the rotator and adds them to the scene.
**
** \param pScene The scene where the numbers are added.
** \param xStart The x-position of the rotator relative to the center of the scene.
*/
void CRotator::buildScene(QGraphicsScene* pScene, int xStart)
{
    if (pScene)
    {
        int count = NUMBER_COUNT;
        int yStart = -count / 2 * NUMBER_ITEM_HEIGHT - NUMBER_ITEM_HEIGHT / 2;
        for (int i = 0; i < count; i++)
        {
            CNumberItem* pItem = new CNumberItem(this, xStart, yStart, false);
            pScene->addItem(pItem);
            m_Numbers.append(pItem);
            yStart += NUMBER_ITEM_HEIGHT;
        }
    }
}

//-----------------------------------------------------------------------------
/**
** Adjusts the position of the numbers.
**
** \param x The y-position of the rotator.
** \param y The y-position of the rotator.
*/
void CRotator::setPosition(int x, int y)
{
    for (int i = 0; i < m_Numbers.count(); i++)
    {
        m_Numbers[i]->setPosition(x, y);
    }
}

//-----------------------------------------------------------------------------
/**
** Enables the reflection all contained numbers.
**
** \param bEnable Set to true to enable, false to disable.
*/
void CRotator::setReflection(bool bEnable)
{
    for (int i = 0; i < m_Numbers.size(); i++)
    {
        m_Numbers[i]->setReflection(bEnable);
    }
}

//-----------------------------------------------------------------------------
/**
** Enables the edit mode in all contained numbers. Sets the background visible
** and the top and bottom numbers.
**
** \param bEnable Set to true to enable, false to disable.
*/
void CRotator::enableEditMode(bool bEnable)
{
    m_bEditMode = bEnable;
    for (int i = 0; i < m_Numbers.size(); i++)
    {
        m_Numbers[i]->showBackground(bEnable);
        if (i - m_Numbers.size() / 2 != 0)
        {
            m_Numbers[i]->setVisible(bEnable);
        }
    }

    if (!bEnable)
    {
        m_Speed = 0;    // Stop rotation when start is clicked
    }
}

//-----------------------------------------------------------------------------
/**
** Sets the fixed values for rotator. By default the rotator shows values from
** 0 to 59 and wraps around. If fixed values has been set for it the rotator
** will show only those and doesn't wrap around.
**
** \param listValues List of fixed values.
*/
void CRotator::setFixedValues(QList<int> listValues)
{
    m_FixedValues = listValues;

    for (int i = 0; i < m_Numbers.size(); i++)
    {
        m_Numbers[i]->showBackground(false);
        m_Numbers[i]->setVisible(true);
        m_Numbers[i]->setDimmed(false);
    }

    m_Speed = 0;

    if (!listValues.isEmpty())
    {
        setOffset(0);
    }
    else
    {
        m_DimmedIndexes.clear();
    }
}

//-----------------------------------------------------------------------------
/**
** Dims the number in given index. Only works with fixed values.
**
** \param index The index to be dimmed.
** \param bDim Set true to dim the index, false to undim it.
*/
void CRotator::setDimIndex(int index, bool bDim)
{
    if (bDim)
    {
        m_DimmedIndexes.append(index);
    }
    else
    {
        m_DimmedIndexes.removeAll(index);
    }
    setOffset(m_Offset, true);
}

//-----------------------------------------------------------------------------
/**
** Sets the opacity for the items.
**
** \param opacity The new opacity value.
*/
void CRotator::setOpacity(qreal opacity)
{
    for (int i = 0; i < m_Numbers.size(); i++)
    {
        m_Numbers[i]->setOpacity(opacity);
    }
}

//-----------------------------------------------------------------------------
/**
** Sets the visibility for the items.
**
** \param bVisible True to show, False to hide.
*/
void CRotator::setVisible(bool bVisible)
{
    for (int i = 0; i < m_Numbers.size(); i++)
    {
        m_Numbers[i]->setVisible(bVisible);
    }
}

//-----------------------------------------------------------------------------
/**
** Sets the value. This just changes the offset to match the height of the number
** item times the value.
**
** \param value The new value which is shown as the central number.
*/
void CRotator::setValue(int value)
{
    // The offset changes also the value
    int offset = value * NUMBER_ITEM_HEIGHT;
    setOffset(offset, true);
}

//-----------------------------------------------------------------------------
/**
** Stops rotation and changes the offset to the nearest number.
*/
void CRotator::stopMovement()
{
    m_Speed = 0;
    int offset = m_Offset + 3;
    offset -= offset % NUMBER_ITEM_HEIGHT;
    setOffset(offset);
    m_DragTimer.stop();
}

//-----------------------------------------------------------------------------
/**
** Sets the new offset for the rotator. Changes the locations of the contained
** numbers with the offset. The number position changes just within the height
** and the value they contain is changed if the offset is greater than the height.
**
** \param offset The new offset for the rotator.
*/
void CRotator::setOffset(int offset)
{
    setOffset(offset, false);
}

//-----------------------------------------------------------------------------
/**
** Sets the new offset for the rotator. Changes the locations of the contained
** numbers with the offset. The number position changes just within the height
** and the value they contain is changed if the offset is greater than the height.
**
** \param offset The new offset for the rotator.
** \param bForce If true the offset is set always even if the value is the same.
*/
void CRotator::setOffset(int offset, bool bForce)
{
    if (sender() && m_Offset == offset && !bForce) return;   // Prevent neverending loop

    if (!m_FixedValues.isEmpty())
    {
        // Limit movement when fixed values are used
        int min = -NUMBER_ITEM_HEIGHT / 3;
        int max = (m_FixedValues.count() - 1) * NUMBER_ITEM_HEIGHT + NUMBER_ITEM_HEIGHT / 3;
        offset = qMax(offset, min);
        offset = qMin(offset, max);

        if (offset == min || offset == max)
        {
            m_Speed = 0;
        }
    }
    else
    {
        offset += NUMBER_ITEM_HEIGHT * m_MaxValue;
        offset %= NUMBER_ITEM_HEIGHT * m_MaxValue;
    }
    m_Offset = offset;

    int remainder = offset % NUMBER_ITEM_HEIGHT;
    int value = offset / NUMBER_ITEM_HEIGHT;

    for (int i = 0; i < m_Numbers.size(); i++)
    {
        int index = i - m_Numbers.size() / 2;
        int v = value - index;

        if (m_FixedValues.isEmpty())
        {
            // Show normal 60 values
            v = (v + m_MaxValue) % m_MaxValue;
        }
        else
        {
            // Show the fixed values
            if (v >= 0 && v < m_FixedValues.count())
            {
                if (m_DimmedIndexes.indexOf(v) != -1)
                {
                    m_Numbers[i]->setDimmed(true);
                }
                else
                {
                    m_Numbers[i]->setDimmed(false);
                }
                v = m_FixedValues[v];
                m_Numbers[i]->setVisible(true);
            }
            else
            {
                m_Numbers[i]->setVisible(false);
            }

        }

//        qDebug() << "value (" << i << "): " << v;

        m_Numbers[i]->setValue(v);
        m_Numbers[i]->setYAdjustment(remainder);
    }

//    qDebug() << m_Offset << m_Numbers[m_Numbers.size() / 2]->value();
    if (m_Numbers[m_Numbers.size() / 2]->isVisible())
    {
        emit valueChanged(m_Numbers[m_Numbers.size() / 2]->value());
    }

    emit offsetChanged(m_Offset);
}

//-----------------------------------------------------------------------------
/**
** Calculates new speed for the rotator.
**
** \param pos The new mouse position.
*/
void CRotator::calculateSpeed(QPointF pos)
{
    int offset = pos.y() - m_DragStartPos.y() + m_DragStartOffset;
    setOffset(offset);

    TimePos tp;
    tp.pos =pos;
    tp.time = QTime::currentTime();
    m_TimePosArray.append(tp);

    if (m_TimePosArray.size() > 3)
    {
        m_TimePosArray.removeFirst();
    }

    // Calculate the speed
    qreal elapsed = m_TimePosArray.first().time.msecsTo(m_TimePosArray.last().time);
    int dist = m_TimePosArray.last().pos.y() - m_TimePosArray.first().pos.y();

    m_Speed = dist / elapsed;
/*
    qDebug() << QTime::currentTime().toString("HH:mm:ss.zzz") <<
            QString("Mouse: %1,%2").arg(pos.x()).arg(pos.y()) <<
            QString("Speed: %1").arg(m_Speed) <<
            QString("Dist: %1").arg(dist) <<
            QString("Elapsed: %1").arg(elapsed);
*/
    DEBUGTEXT("Speed", QString("%1").arg(m_Speed));
    DEBUGTEXT("Offset", QString("%1").arg(m_Offset));
}

///////////////////////////////////////////////////////////////////////////////
/// EVENT HANDLERS
///////////////////////////////////////////////////////////////////////////////

//-----------------------------------------------------------------------------
/**
** Handler for the mouse movement. Changes the speed accoring to the previous
** mouse position.
**
** \param pEvent The mouse event
*/
void CRotator::mouseMoveEvent(QGraphicsSceneMouseEvent* pEvent)
{
    if (pEvent && (m_bEditMode || !m_FixedValues.isEmpty()))
    {
        calculateSpeed(pEvent->pos());
    }

    // If the mouse moves too far stop the tap and hold timer
    QPointF pos = pEvent->pos() - m_DragStartPos;
    if (QPoint(pos.x(), pos.y()).manhattanLength() > 40)
    {
        m_HoldTimer.stop();
    }
}

//-----------------------------------------------------------------------------
/**
** Handles the mouse press events. Stores the position where the mouse was pressed
** for the dragging.
**
** \param pEvent The mouse event
*/
void CRotator::mousePressEvent(QGraphicsSceneMouseEvent* pEvent)
{
/*
    qDebug() << QTime::currentTime().toString("HH:mm:ss.zzz") <<
            QString("Mouse: %1,%2 (press)").arg(pEvent->pos().x()).arg(pEvent->pos().y()) <<
            QString("Speed: %1").arg(m_Speed);
*/
    if (pEvent && (m_bEditMode || !m_FixedValues.isEmpty()))
    {
        m_DragStartOffset = m_Offset;
        m_DragStartPos = pEvent->pos();
        m_DragTimer.stop();

        m_TimePosArray.clear();
        TimePos tp;
        tp.pos = pEvent->pos();
        tp.time = QTime::currentTime();
        m_TimePosArray.append(tp);

        if (!m_FixedValues.isEmpty())
        {
            // Start the hold timer
            m_HoldTimer.start(TAP_AND_HOLD_TIMEOUT);
        }
    }
}

//-----------------------------------------------------------------------------
/**
** Handler for the mouse release events. Starts timer to adjust the speed
** of the rotator.
**
** \param pEvent The mouse event
*/
void CRotator::mouseReleaseEvent(QGraphicsSceneMouseEvent* pEvent)
{
    if (pEvent && (m_bEditMode || !m_FixedValues.isEmpty()))
    {
        if (m_TimePosArray.size() > 1)      // Filter events without any "Move"
        {
            calculateSpeed(pEvent->pos());
        }
        else if (m_TimePosArray.size() == 1 && m_TimePosArray[0].pos == pEvent->pos())
        {
            m_Speed = 0;    // Tap stops
        }
    }

    // Start the timer to align the items
    m_DragTimer.start(DRAG_TIMEOUT_RATE);
    m_HoldTimer.stop();
}

///////////////////////////////////////////////////////////////////////////////
/// SLOTS
///////////////////////////////////////////////////////////////////////////////

//-----------------------------------------------------------------------------
/**
** Slot which gets called when the drag timer time outs. Adjusts the offset
** for the current speed. If the speed is low enough the offset is changed
** to the nearest item.
*/
void CRotator::onTimeout()
{
    DEBUGTEXT("Speed", QString("%1").arg(m_Speed));
    DEBUGTEXT("Offset", QString("%1").arg(m_Offset));

    if (m_Speed > 0.3 || m_Speed < -0.3)
    {
        // Add the speed to the offset to make the rotator go
        setOffset(m_Offset + m_Speed * DRAG_TIMEOUT_RATE);     // Speed is pixels / millisecond and the timer runs in every DRAG_TIMEOUT_RATE ms
//        m_Speed *= 0.95;    // Slow down
    }
    else
    {
        // If the speed is low enough just move to the nearest full item

        int remainder = (m_Offset + NUMBER_ITEM_HEIGHT) % NUMBER_ITEM_HEIGHT;

        if (remainder > 1 && remainder < NUMBER_ITEM_HEIGHT - 1)
        {
            // Prefer the scroll direction
            int limit = NUMBER_ITEM_HEIGHT / 2;
            if (m_Speed > 0)
            {
                limit -= NUMBER_ITEM_HEIGHT / 6;
            }
            else
            {
                limit += NUMBER_ITEM_HEIGHT / 6;
            }

            // Adjust the offset
            if (remainder > limit)
            {
                int adjust = (NUMBER_ITEM_HEIGHT - remainder) * 0.3;
                adjust = qMax(1, adjust);
                setOffset(m_Offset + adjust);
            }
            else
            {
                int adjust = remainder * 0.3;
                adjust = qMax(1, adjust);
                setOffset(m_Offset - adjust);
            }
        }
        else
        {
            // The offset is set for a full item
            int offset = m_Offset + 3;
            offset -= offset % NUMBER_ITEM_HEIGHT;
            setOffset(offset);
            m_DragTimer.stop();
        }
    }
}

//-----------------------------------------------------------------------------
/**
** Slot which is triggered when the mouse has been pressed down for a while.
*/
void CRotator::onHoldTimeout()
{
    m_HoldTimer.stop();
    int index = int((m_Offset + (NUMBER_ITEM_HEIGHT / 2)) / NUMBER_ITEM_HEIGHT - m_DragStartPos.y() / NUMBER_ITEM_HEIGHT + 0.5);
//    qDebug() << "Hold at " << m_DragStartPos << " " << m_Offset << " " << m_Value << " " << index;

    emit holdIndex(index);
}

// EOF
